﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using DomainDriver.DomainModeling.DomainObjects;

namespace DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage
{
    public class EfAggregateUtilities
    {
        public static ICollection<NESTED_OBJECT> ConvertSingleNestedObjectToList<NESTED_OBJECT>(NESTED_OBJECT singleNestedObject)
            where NESTED_OBJECT : class
        {
            List<NESTED_OBJECT> nestedObjectList = new List<NESTED_OBJECT>();
            if (singleNestedObject != null)
            { nestedObjectList.Add(singleNestedObject); }
            return nestedObjectList;
        }

        public static IDictionary<NESTED_OBJECT_KEY, NESTED_OBJECT> ConvertSingleNestedObjectToDictionary<NESTED_OBJECT_KEY, NESTED_OBJECT>(NESTED_OBJECT singleNestedObject, Expression<Func<NESTED_OBJECT, NESTED_OBJECT_KEY>> keyGetterMethod)
            where NESTED_OBJECT : class
        {
            Dictionary<NESTED_OBJECT_KEY, NESTED_OBJECT> nestedObjectDictionary = new Dictionary<NESTED_OBJECT_KEY, NESTED_OBJECT>();
            if (singleNestedObject != null)
            {
                NESTED_OBJECT_KEY key = keyGetterMethod.Compile()(singleNestedObject);
                nestedObjectDictionary.Add(key, singleNestedObject);
            }
            return nestedObjectDictionary;
        }

        public static void AddNestedValues<NESTED_OBJECT>(DbContext context, Expression<Func<ICollection<NESTED_OBJECT>>> nestedObjectsGetterExpression)
            where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            ICollection<NESTED_OBJECT> nestedObjects = nestedObjectsGetterExpression.Compile()();
            foreach (NESTED_OBJECT nestedObject in nestedObjects)
            {
                AddNestedValues<NESTED_OBJECT>(context, nestedObject);

                context.Set<NESTED_OBJECT>().Add(nestedObject);
            }
        }

        public static void UpdateNestedValues<AGGREGATE_ROOT, KEY, NESTED_OBJECT>(DbContext context, AGGREGATE_ROOT updatedAggregateRoot, AGGREGATE_ROOT originalAggregateRoot, Expression<Func<NESTED_OBJECT, KEY>> keyGetterExpression, Expression<Func<NESTED_OBJECT, NESTED_OBJECT, bool>> nestedObjectsMatchExpression, Expression<Func<AGGREGATE_ROOT, IDictionary<KEY, NESTED_OBJECT>>> dictionaryGetterExpression)
            where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            DbSet<NESTED_OBJECT> dbSet = context.Set<NESTED_OBJECT>();
            IDictionary<KEY, NESTED_OBJECT> updatedDictionary = dictionaryGetterExpression.Compile()(updatedAggregateRoot);
            IDictionary<KEY, NESTED_OBJECT> originalDictionary = dictionaryGetterExpression.Compile()(originalAggregateRoot);
            IDictionary<KEY, NESTED_OBJECT> entriesToDelete = new Dictionary<KEY, NESTED_OBJECT>(originalDictionary);

            foreach (NESTED_OBJECT nestedObject in updatedDictionary.Values)
            {
                KEY keyForValue = keyGetterExpression.Compile()(nestedObject);

                if (!entriesToDelete.ContainsKey(keyForValue))
                {
                    AddNestedValues<NESTED_OBJECT>(context, nestedObject);

                    dbSet.Add(nestedObject);
                }
                else
                {
                    object[] objectKey = null;
                    if (nestedObject is IEfStorableObject)
                    {
                        objectKey = (nestedObject as IEfStorableObject).EF_CombinedId;
                    }
                    else
                    {
                        // LATER: Use reflection to automatically gather properties marked with "[Key]"
                    }

                    // NOTE: Copy updated value to original
                    if ((objectKey != null) && (objectKey.Length > 0))
                    {
                        NESTED_OBJECT originalObject = dbSet.Find(objectKey);
                        NESTED_OBJECT originalObjectCopy = originalObject.Copy();
                        nestedObject.CopyTo(originalObject);

                        UpdatedNestedValues<NESTED_OBJECT>(context, nestedObject, originalObjectCopy);
                    }

                    // NOTE: Updated objects should not be deleted
                    entriesToDelete.Remove(keyForValue);
                }
            }

            foreach (NESTED_OBJECT valueToDelete in entriesToDelete.Values)
            {
                Expression<Func<NESTED_OBJECT, bool>> queryExpression = ((NESTED_OBJECT val) => nestedObjectsMatchExpression.Compile()(val, valueToDelete));
                IEnumerable<NESTED_OBJECT> queryResults = context.Set<NESTED_OBJECT>().Where<NESTED_OBJECT>(queryExpression.Compile());

                if (queryResults.Count<NESTED_OBJECT>() > 0)
                {
                    NESTED_OBJECT matchingValue = queryResults.First<NESTED_OBJECT>();
                    dbSet.Remove(matchingValue);

                    RemoveNestedValues<NESTED_OBJECT>(context, matchingValue);
                }
            }
        }

        public static void DeleteNestedValues<NESTED_OBJECT>(DbContext context, Expression<Func<NESTED_OBJECT, bool>> deleteQueryExpression)
            where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            IEnumerable<NESTED_OBJECT> deleteQueryResults = context.Set<NESTED_OBJECT>().Where<NESTED_OBJECT>(deleteQueryExpression.Compile());
            List<NESTED_OBJECT> nestedObjectsToDelete = new List<NESTED_OBJECT>(deleteQueryResults);
            foreach (NESTED_OBJECT nestedObject in nestedObjectsToDelete)
            {
                context.Set<NESTED_OBJECT>().Remove(nestedObject);

                RemoveNestedValues<NESTED_OBJECT>(context, nestedObject);
            }
        }

        private static void AddNestedValues<NESTED_OBJECT>(DbContext context, NESTED_OBJECT nestedObject) where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            if (!(nestedObject is IEfAggregate<NESTED_OBJECT>))
            { return; }
            (nestedObject as IEfAggregate<NESTED_OBJECT>).AddNestedAggregateValues(context);
        }

        private static void UpdatedNestedValues<NESTED_OBJECT>(DbContext context, NESTED_OBJECT nestedObject, NESTED_OBJECT originalObjectCopy) where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            if (!(nestedObject is IEfAggregate<NESTED_OBJECT>))
            { return; }
            (nestedObject as IEfAggregate<NESTED_OBJECT>).UpdateNestedAggregateValues(context, originalObjectCopy);
        }

        private static void RemoveNestedValues<NESTED_OBJECT>(DbContext context, NESTED_OBJECT nestedObject) where NESTED_OBJECT : class, IDomainObject<NESTED_OBJECT>
        {
            if (!(nestedObject is IEfAggregate<NESTED_OBJECT>))
            { return; }
            (nestedObject as IEfAggregate<NESTED_OBJECT>).DeleteNestedAggregateValues(context);
        }
    }
}